15장 더하고 빼는 맞춤 동작
출처: 원서 『API Design Patterns』(JJ Geewax, Manning).
이 장에서 딱 3가지만 1. 관계에 메타데이터가 필요 없을 때, 연관 리소스 대신 add/remove 동작으로 더 단순하게 다대다 관계를 다루는 법을 설명한다. 2. add/remove를 어느 쪽 리소스에 붙일지(관리 리소스 선택)를 구분한다. 3. 중복으로 넣거나 없는 걸 빼려 할 때 어떤 응답을 줘야 하는지를 설명한다.
이 장의 학습 목표는 세 가지입니다. - add/remove 동작이 연관 리소스보다 단순한 이유를 설명한다. - "그룹에 사용자를 넣는다"처럼 add/remove를 어느 리소스에 붙일지 구분한다. - 중복 추가·없는 것 제거 같은 무결성 상황의 응답 코드를 설명한다.
들어가기 전에 — 또 멤버십 카드를 만들어야 해?
지난 장(14장)에서 우리는 "사용자가 그룹에 속한다" 같은 다대다 관계를 다루려고 연관 리소스라는 걸 따로 만들었습니다.
그런데 막상 써 보니 이런 적 있죠?
그냥 "이 사람을 이 그룹에 넣고 싶을 뿐"인데, 멤버십이라는 리소스를 만들고, 그 멤버십을 또 Get·Update·Delete·List 하는 메서드를 줄줄이 만들고… 메서드가 일곱 개나 됐습니다.
역할도 가입일도 필요 없는데 말이죠.
그게 바로 이 장이 풀려는 문제입니다. 관계에 추가로 적을 정보가 없을 때는, 멤버십 카드 같은 걸 만들지 말고 그냥 "넣고(add)" "빼는(remove)" 동작 두 개만 주자는 거예요.
이렇게 관계의 존재 여부만 다루는 더하기·빼기 동작, 이게 add/remove 동작입니다. (0장 용어집 21번)
비유 먼저 — 멤버십 카드 vs 그냥 초대
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| 동호회에 "회원 카드"를 발급하고 가입일·등급을 적어 둠 | 14장 연관 리소스 (Membership) | 메서드가 7개로 늘어 무거움 |
| 카톡 단톡방에 사람을 그냥 "초대"하고 "내보내기" | 15장 add/remove 동작 | 역할·가입일 같은 정보는 못 적음 |
| 명함첩에 이름만 꽂아 둠 (관계 종류는 안 적음) | 관계의 존재 여부만 저장 | "친구인지 동료인지"는 알 수 없음 |
핵심 한 줄: 적을 정보가 없으면, 카드를 만들지 말고 더하기·빼기만 하자.
14장: CreateMembership({ 사용자: "김철수", 그룹: "반모임", 역할: "관리자" })
→ 멤버십 리소스 생성. 이후 Get·Update·Delete·List까지 줄줄이.
15장: AddGroupUser({ 그룹: "groups/반모임", 사용자: "users/김철수" })
→ 끝! 추가 리소스 없음. add·remove·목록만 있으면 됨.
1. 언제 더하기·빼기로 충분할까 — 메타데이터가 없을 때
이런 적 있죠?
게시글에 태그를 달려고 하는데, 누가 "이 태그-게시글 연결의 가입일이 언제예요? 역할은요?"라고 물으면 황당하죠.
태그는 그냥 붙어 있거나(O) 안 붙어 있거나(X) 둘 중 하나입니다. 역할도 만료일도 없습니다. 그게 전부예요.
이렇게 관계에 딸려 적을 정보(메타데이터)가 없는 경우, 멤버십 카드 같은 연관 리소스는 과합니다.
메타데이터가 필요 없는 다대다 관계 예 5가지
1. 소셜 미디어 팔로우
사용자 ↔ 사용자 (팔로우한다/안 한다, 역할 없음)
→ AddUserFollower / RemoveUserFollower
2. 게시글 태그
게시글 ↔ 태그 (붙어 있다/없다)
→ AddPostTag / RemovePostTag
3. 상품 카테고리
상품 ↔ 카테고리 (소속 여부만)
→ AddCategoryProduct / RemoveCategoryProduct
4. 재생목록 영상
재생목록 ↔ 영상 (들어 있다/없다)
→ AddPlaylistVideo / RemovePlaylistVideo
5. 사용자 역할 (역할이 문자열이 아니라 별도 리소스일 때)
사용자 ↔ 역할 (가지고 있다/없다)
→ AddUserRole / RemoveUserRole
한눈에 — 14장이 좋은 경우 vs 15장이 좋은 경우
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| 회원증에 등급·가입일을 적어야 함 | 14장 연관 리소스 | 관계에 정보가 정말 있을 때만 |
| 단톡방 초대처럼 들었다/안 들었다만 | 15장 add/remove | 나중에 정보가 필요해지면 갈아엎어야 함 |
한 걸음 더 ▸ 정보가 없어도 14장을 쓸 수 있다 메타데이터가 없어도 연관 리소스(14장)를 쓰는 게 틀린 건 아닙니다. 다만 메서드가 더 많아 무겁죠. "단순하게 가고 싶다"가 15장을 고르는 이유입니다.
2. 더하기·빼기를 어느 쪽에 붙일까 — 관리 리소스 고르기
이런 적 있죠?
"그룹에 사용자를 추가"인지 "사용자에 그룹을 추가"인지, 둘 다 말이 되는 것 같아서 어디에 메서드를 달지 한참 고민한 적 있죠?
여기서 새 용어 하나가 나옵니다.
관리 리소스란, 관계를 관리하는 쪽 리소스입니다. 쉽게 말해 add/remove 버튼이 달리는 쪽이에요. (단톡방에서 방장 역할을 맡는 그 방 자체라고 보면 됩니다.) 그리고 거기에 더해지고 빠지는 쪽을 연관 대상 리소스라고 부릅니다. (방에 초대되는 손님 쪽이죠.)
둘 중 하나만 고른다
방식 1: 그룹이 관리 리소스 (그룹이 사용자를 관리)
─────────────────────────────────────────────
POST /groups/1/users:add { userId: "users/alice" } ← "그룹에 사용자를 추가"
POST /groups/1/users:remove { userId: "users/alice" } ← "그룹에서 사용자를 제거"
방식 2: 사용자가 관리 리소스 (사용자가 그룹을 관리)
─────────────────────────────────────────────
POST /users/alice/groups:add { groupId: "groups/1" } ← "사용자에 그룹을 추가"
POST /users/alice/groups:remove { groupId: "groups/1" } ← "사용자에서 그룹을 제거"
방식 1과 방식 2를 둘 다 주면 안 됩니다. 같은 일을 두 군데서 할 수 있어 헷갈리거든요. 딱 한쪽만 고릅니다.
고르는 요령 — "A에 B를 넣는다"가 자연스러운 쪽이 A
질문은 딱 하나입니다. "누가 관계를 관리하는 게 자연스러운가?"
관계 관리 리소스 왜
────────────────────────────────────────────────
그룹 ↔ 사용자 그룹 "그룹에 사람을 초대"
재생목록 ↔ 영상 재생목록 "재생목록에 영상 추가"
게시글 ↔ 태그 게시글 "게시글에 태그 달기"
사용자 ↔ 팔로워 사용자(대상) "이 사용자를 팔로우"
상품 ↔ 카테고리 카테고리 "카테고리에 상품 배치"
장바구니 ↔ 상품 장바구니 "장바구니에 상품 담기"
비유로 연결
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| 단톡방 방장만 초대/강퇴 가능 | 그룹이 관리 리소스 | 멤버 본인은 스스로 못 들어옴 |
| 재생목록 주인이 영상을 끼워 넣음 | 재생목록이 관리 리소스 | 영상 쪽엔 add 버튼이 없음 |
| 양쪽 다 초대 버튼을 달면? | 방식 1+2 동시 제공 | 같은 일이 두 군데, 혼란 |
핵심 한 줄: "A에 B를 추가한다"가 입에 더 붙는 쪽이 A(관리 리소스)다.
잠깐 — 팔로우는 사실 양쪽 다 말이 된다 (정답 1개가 아님)
위 표에서 팔로우는 "사용자(대상)"이 관리 리소스라고 적어 뒀습니다. 그런데 이걸 외워야 할 정답으로 받아들이면 곤란해요. 팔로우는 양쪽 다 말이 되거든요.
먼저 이런 적 있죠? 인스타에서 누군가를 팔로우할 때, "내가 그 사람을 내 팔로잉 목록에 넣는 것"처럼 느껴지기도 하고, 동시에 "그 사람의 팔로워 목록에 내가 들어가는 것"처럼 느껴지기도 하죠. 둘 다 자연스럽습니다. 그게 정상이에요.
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| "그 사람의 팔로워 명단에 나를 올린다" | 받는 사용자가 관리 리소스 → AddUserFollower | 한쪽만 외우면 다른 설계를 틀렸다고 오해 |
| "내 팔로잉 명단에 그 사람을 올린다" | 하는 사용자가 관리 리소스 → AddUserFollowing | 도메인에 따라 이쪽이 더 자연스러울 수도 |
그래서 어느 쪽이 맞느냐고요? 둘 다 맞습니다. 어느 쪽으로 둘지는 우리 서비스의 도메인과 팀 합의로 정하는 거예요.
잘못된 사고 (기계적 규칙으로 외움)
"팔로우 = 받는 사용자가 관리 리소스. 끝. 외우자."
→ AddUserFollowing 설계를 보고 "이건 틀렸다"고 단정 → 틀린 판단
올바른 사고 (양쪽 검토 후 합의)
"팔로우는 받는 쪽·하는 쪽 둘 다 관리 리소스가 될 수 있다.
우리 서비스에선 '내 팔로잉 관리'가 주 화면이니 하는 쪽으로 가자."
→ 팀과 합의해 한쪽을 고름 → 근거 있는 판단
한 걸음 더 ▸ '자연스러운 쪽'은 사람마다 다를 수 있다 §2의 "A에 B를 넣는 게 자연스러운 쪽" 요령은 대부분 잘 맞습니다. 다만 팔로우처럼 양쪽이 팽팽하면 요령만으로 결론이 안 납니다. 그럴 땐 "외운 답"을 꺼내지 말고, 양쪽을 적어 보고 팀과 합의하는 게 정답입니다.
핵심 한 줄: 팔로우는 받는 쪽·하는 쪽 둘 다 관리 리소스가 될 수 있다 — 정답은 팀 합의다.
3. 어떻게 생겼나 — add/remove 메서드 만들기
사용자 메서드 모양 그대로
add/remove는 새로운 발명이 아닙니다. 0장 용어집 8번의 사용자 메서드(표준 틀 밖의 맞춤 동작) 모양을 그대로 빌려 옵니다.
즉 POST + 콜론(:) + 동작 이름이에요.
표 — Add / Remove 메서드 요약
액션 메서드 HTTP
─────────────────────────────────────────────────────────────────
Alice를 그룹 1에 추가 AddGroupUser({ POST /groups/1/users:add
parent: "groups/1", Body: { userId: "users/1" }
userId: "users/1" }) → 200 OK (반환값 없음)
Alice를 그룹 1서 제거 RemoveGroupUser({ POST /groups/1/users:remove
parent: "groups/1", Body: { userId: "users/1" }
userId: "users/1" }) → 200 OK (반환값 없음)
이름 짓는 규칙
규칙은 단순합니다. Add/Remove + 관리리소스(단수) + 대상리소스(단수)
- AddGroupUser (그룹에 사용자 추가)
- RemoveGroupUser (그룹에서 사용자 제거)
- AddPlaylistVideo (재생목록에 영상 추가)
- RemovePostTag (게시글에서 태그 제거)
잘못된 예 vs 올바른 예 — 본문에 뭘 담을까
요청 본문에는 추가할 대상의 식별자만 담습니다. 전체 정보를 다 담으면 안 됩니다.
잘못된 예: 전체 리소스를 다 담음
POST /groups/1/users:add
{
user: {
id: "users/alice",
name: "Alice", ← 불필요
email: "alice@test.com" ← 이걸 보내면 User도 수정되는 건가? 혼란!
}
}
올바른 예: 식별자만 담음
POST /groups/1/users:add
{
userId: "users/alice" ← 추가할 대상 식별자만!
}
왜 식별자만 담을까요? 나머지 정보는 "관계를 맺는다"는 본래 목적과 상관이 없고, 그걸 받으면 클라이언트가 "어, 이걸 보내면 사용자 정보도 같이 고쳐지나?" 하고 오해할 수 있기 때문입니다.
비유로 연결
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| 단톡방 초대에 전화번호(ID)만 입력 | userId만 본문에 담기 | 이름·이메일까지 보내면 오해 |
| 초대했다고 그 사람 프로필이 바뀌진 않음 | add는 관계만 만든다 | 본문에 프로필 담으면 수정처럼 보임 |
4. 양쪽으로 보기 — 그룹의 사용자, 사용자의 그룹
목록은 양방향으로
관계를 만들었으면 양쪽에서 다 볼 수 있어야겠죠. "이 그룹에 누가 있나?"도, "이 사용자는 어느 그룹에 있나?"도 둘 다요.
표 — list 메서드 요약
질문 메서드 HTTP
─────────────────────────────────────────────────────────────────
그룹 1에 속한 사용자는? ListGroupUsers({ GET /groups/1/users
parent: "groups/1" }) → User[] 반환
사용자 1이 속한 그룹은? ListUserGroups({ GET /users/1/groups
parent: "users/1" }) → Group[] 반환
여기서 중요한 점 하나. 돌아오는 건 User[] / Group[] (진짜 리소스)입니다. 멤버십 같은 중간 카드가 아니에요. 그런 카드는 애초에 만들지 않았으니까요.
새 용어 — 암시적 서브컬렉션
GET /groups/1/users 이 주소를 보면 마치 users가 그룹의 서브리소스(0장 용어집 15번)인 것처럼 보입니다.
그런데 사실은 아닙니다.
암시적 서브컬렉션이란, 진짜 서브리소스는 아닌데 주소만 서브컬렉션처럼 생긴 구조를 말합니다. (방 안에 진짜로 들어 있는 게 아니라, "이 방과 관련된 사람들"을 보여 주는 안내판 같은 거예요.)
GET /groups/1/users
→ users가 그룹의 진짜 서브리소스처럼 보이지만,
실제로는 "그룹 1과 연관된 사용자 목록"을 보여 주는 편의 주소다.
진짜 서브리소스: /groups/1/settings (그룹이 소유)
암시적 서브컬렉션: /groups/1/users (그룹과 연관된 사용자)
사람이 많으면 — 페이징
그룹에 사용자가 수천 명일 수 있으니, 한 번에 다 주지 않고 끊어서 줍니다.
GET /groups/1/users?maxPageSize=50
→ {
users: [ ...최대 50명... ],
nextPageToken: "eyJ0..." ← 다음 페이지 열쇠
}
다음 페이지
GET /groups/1/users?maxPageSize=50&pageToken=eyJ0...
→ {
users: [ ... ],
nextPageToken: null ← 마지막 페이지
}
비유로 연결
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| 단톡방 멤버 목록 / 내가 든 단톡방 목록 | 양방향 list 메서드 | 한쪽만 만들면 반대편 조회 불가 |
| 방 안내판("관련 인물")은 방 내부물이 아님 | 암시적 서브컬렉션 | 진짜 서브리소스로 오해 금지 |
| 멤버 5천 명을 한 화면에 다 못 띄움 | 페이징(nextPageToken) | 페이징 빼면 거대 응답으로 터짐 |
그룹 본문에 멤버를 통째로 박지 마라 — 개수만, 목록은 따로
이런 적 있죠? 그룹 하나를 GetGroup으로 불러왔는데, 그 안에 멤버 5천 명이 배열로 통째로 딸려 와서 응답이 수 메가바이트가 된 적.
그룹 본문(Group 리소스)에 멤버 목록을 배열로 끼워 넣는(인라인) 건 함정입니다. 관계가 많아지면 그 그룹을 조회할 때마다 멤버 전체가 따라와 응답이 터지거든요.
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| 동호회 소개판에 회원 전원 이름을 다 박음 | Group 본문에 users 배열 인라인 | 회원 5천이면 소개판 한 장에 안 들어감 |
| 소개판엔 "회원 32명"만, 명단은 게시판에서 따로 | Group엔 userCount만, 목록은 list 메서드 | 본문 가볍고, 명단은 페이징으로 끊어서 |
그래서 규칙은 이렇습니다. 본문엔 개수(userCount)만 두고, 실제 목록은 §4 앞에서 본 list 메서드로 따로 받자.
잘못된 예: 그룹 본문에 멤버 목록을 배열로 인라인
GET /groups/1
→ {
id: "groups/1",
name: "엔지니어링",
users: [ ← 멤버 전부를 본문에 박음
{ id: "users/alice", ... },
{ id: "users/bob", ... },
...5천 명... ← 그룹 한 번 부를 때마다 5천 명이 따라옴!
]
}
올바른 예: 개수만 본문에, 목록은 별도 메서드
GET /groups/1
→ {
id: "groups/1",
name: "엔지니어링",
userCount: 5000 ← 개수만! 본문이 가볍다
}
실제 명단이 필요하면 따로 (페이징으로 끊어서)
GET /groups/1/users?maxPageSize=50 ← ListGroupUsers
→ { users: [ ...최대 50명... ], nextPageToken: "..." }
# 잘못된 예 — 본문에 목록을 통째로 담는 응답
def get_group_bad(group_id):
group = db.get_group(group_id)
group["users"] = db.all_members(group_id) # 5천 명을 그대로 붙임
return group # 그룹 조회마다 거대 응답
# 올바른 예 — 개수만 담고, 목록은 list 메서드에 맡김
def get_group_good(group_id):
group = db.get_group(group_id)
group["userCount"] = db.count_members(group_id) # 숫자 하나뿐
return group # 가벼움. 명단은 ListGroupUsers로 따로 받음
핵심 한 줄: 관계가 많으면 본문엔 개수(userCount)만, 명단은 list 메서드로.
5. 실수했을 때 — 중복 추가와 없는 것 제거
이런 적 있죠?
이미 단톡방에 있는 사람을 또 초대 버튼 눌렀더니 그냥 조용히 넘어가거나 에러가 나거나… 서버마다 제멋대로면 클라이언트가 헷갈리죠.
그래서 표준 규칙을 정해 둡니다.
기본 규칙 (안전판) — 상황별 응답 코드
상황 1: 정상 추가
POST /groups/1/users:add { userId: "users/jimmy" }
→ 200 OK (Jimmy가 그룹 1에 추가됨)
상황 2: 중복 추가 (이미 멤버)
POST /groups/1/users:add { userId: "users/jimmy" } (두 번째)
→ 409 Conflict ("이미 멤버입니다")
상황 3: 정상 제거
POST /groups/1/users:remove { userId: "users/jimmy" }
→ 200 OK (Jimmy가 그룹 1에서 제거됨)
상황 4: 없는 사람 제거
POST /groups/1/users:remove { userId: "users/unknown" }
→ 412 Precondition Failed ("이 사용자는 이 그룹의 멤버가 아닙니다")
상황 5: 존재하지 않는 사용자를 추가
POST /groups/1/users:add { userId: "users/nonexistent" }
→ 404 Not Found ("해당 사용자가 존재하지 않습니다")
외울 건 딱 세 개입니다. - 중복 추가 → 409 Conflict - 없는 것 제거 → 412 Precondition Failed - 아예 없는 대상 → 404 Not Found
409가 떠도 사실 목적은 이뤄졌다
여기서 0장 용어집 10번의 멱등성이 빛납니다. 같은 add를 두 번 보내서 두 번째에 409가 떠도, 결과적으로 Jimmy는 그룹의 멤버가 맞습니다.
add 1번 → 200 OK (Jimmy 멤버됨)
add 2번 → 409 충돌 (이미 멤버)
어느 쪽이든 결과: Jimmy는 그룹 1의 멤버. 목적 달성!
그래서 "관계를 만들고 싶다"가 목적이라면, 클라이언트는 409도 성공으로 취급해도 됩니다.
14장과 비교 — 안은 같고 겉만 단순
14장 (연관 리소스):
CreateMembership({ userId: "jimmy", groupId: "1" }) → 201 Created
CreateMembership({ userId: "jimmy", groupId: "1" }) → 409 Conflict
→ 멤버십이 명시적 리소스라 중복 검증도 눈에 보임
15장 (add/remove):
AddGroupUser({ parent: "groups/1", userId: "users/jimmy" }) → 200 OK
AddGroupUser({ parent: "groups/1", userId: "users/jimmy" }) → 409 Conflict
→ 안에서는 관계를 저장하지만 리소스로 드러내지 않음
→ 검증 로직은 같고, 클라이언트가 보기엔 더 단순
이렇게 중복을 막는 것을 0장 용어집 26번의 고유성 제약이라고 합니다. (같은 짝이 두 번 생기지 않게 막는 규칙이죠.)
비유로 연결
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| 이미 든 사람을 또 초대 | 409 Conflict | 조용히 무시하면 클라이언트가 헷갈림 |
| 없는 사람을 내보내기 | 412 Precondition Failed | 200으로 속이면 진짜 처리된 줄 오해 |
| 두 번 눌러도 결국 멤버는 멤버 | 멱등성 | 409를 무조건 실패로 보면 재시도 지옥 |
한 걸음 더 ▸ 빈칸 채우기 아래 빈칸에 응답 코드를 채워 보세요. - 이미 있는 태그를 또 add →
___ Conflict- 안 달린 태그를 remove →___ Precondition Failed- 존재하지도 않는 태그 ID를 add →___ Not Found(정답: 409 / 412 / 404)
6. 단순함의 대가 — 무엇을 포기했나
add/remove는 단순해서 좋지만, 공짜는 아닙니다. 두 가지를 내려놓아야 해요.
6-1. 한쪽으로만 가능 — 비상호관계
새 용어가 또 하나 나옵니다.
비상호관계란, 관리 리소스를 한쪽에만 둬서 양방향이 대칭이 아닌 상태를 말합니다. (단톡방에서 방장만 초대할 수 있고, 손님은 스스로 못 들어오는 그 비대칭이에요.)
그룹이 관리 리소스일 때:
가능: POST /groups/1/users:add ← "그룹에 사용자를 추가" (자연스러움)
불가: POST /users/alice/groups:add ← 이런 메서드는 없음!
→ "Alice를 engineering 그룹에 넣고 싶다"면
POST /groups/engineering/users:add 를 호출해야 한다.
POST /users/alice/groups:add 는 못 쓴다.
6-2. 관계에 정보를 못 적음
14장 (연관 리소스): 관계에 정보 저장 가능
{
userId: "alice",
groupId: "engineering",
role: "admin", ← 역할
joinedAt: "2024-03-15", ← 가입일
expireTime: "2025-03-15" ← 만료일
}
15장 (add/remove): 존재 여부만
"Alice는 engineering의 멤버이다" (O)
"Alice는 engineering의 admin이다" (X) ← 못 적음!
"Alice가 2024-03-15에 가입했다" (X) ← 못 적음!
이게 0장 용어집 20번의 관계 메타데이터입니다. add/remove는 이걸 담을 자리가 없어요.
정보를 꼭 적어야 한다면? 14장의 연관 리소스로 갈아타거나, 관계와 별도로 그 정보를 저장할 다른 방법을 찾아야 합니다.
비유로 연결
| 비유 | API/코드 | 위험·주의 |
|---|---|---|
| 방장만 초대 가능(손님은 못 들어옴) | 비상호관계 | 반대편에서 호출하려다 막힘 |
| 명함첩에 이름만, 관계 종류는 못 적음 | 관계 메타데이터 불가 | 나중에 역할 필요해지면 갈아엎기 |
7. 그래서 14장이냐 15장이냐 — 고르는 순서도
마지막으로 둘 중 무엇을 고를지 정리합니다.
Q1: 관계에 정보(역할·가입일 등)가 필요한가?
├── 예 → 14장 연관 리소스 (멤버십 패턴)
│
└── 아니오 → Q2: API를 최대한 단순하게 하고 싶은가?
├── 예 → 15장 add/remove
└── 아니오 → 14장 연관 리소스 (정보 없이도 쓸 수 있음)
Q3: 나중에 정보가 필요해질 가능성이 큰가?
├── 예 → 처음부터 14장 (나중에 갈아엎는 비용 방지)
└── 아니오 → 15장으로 시작, 필요해지면 14장으로 전환
한눈에 비교표 (14장 vs 15장)
| 항목 | 14장 연관 리소스 | 15장 add/remove |
|---|---|---|
| 중간 관계 리소스 | 멤버십(별도) | 없음(숨김) |
| 관계 관련 메서드 수 | 최대 7개 | 4개 |
| 관계에 정보 적기 | 가능 | 불가 |
| 복잡도 | 높음 | 낮음 |
| 양쪽으로 조회 | 별칭 메서드 | list 메서드 |
| 양방향 대칭 | 대칭 | 비대칭(한쪽만 관리) |
| 목록 반환 타입 | 멤버십[] | User[] / Group[] |
| 중복 생성 | 409 Conflict | 409 Conflict |
| 적합한 때 | 정보가 필요할 때 | 존재 여부만 필요할 때 |
메서드 개수로 느끼는 단순함
User 관련: 5개 (Create·Get·List·Update·Delete)
Group 관련: 5개 (Create·Get·List·Update·Delete)
관계 관련: 4개 (Add·Remove·ListGroupUsers·ListUserGroups)
────────────────────────────────────────────
총 14개 — 14장(17개)보다 3개 적다!
8. 스스로 풀어 보기 — 워크드 예제에서 독립 적용까지
워크드 예제 (다 보여 줌)
문제: Recipe와 Ingredient의 다대다 관계에서 관리 리소스는 어느 쪽일까?
풀이: "레시피에 재료를 추가한다"가 자연스럽습니다. 그러니 Recipe가 관리 리소스예요. - AddRecipeIngredient / RemoveRecipeIngredient - ListRecipeIngredients (이 레시피에 필요한 재료) - ListIngredientRecipes (이 재료를 쓰는 레시피)
부분 완성 (빈칸 채우기)
문제: 팔로우 관계의 메서드 이름을 채워 보세요.
"이 사용자에 팔로워를 추가" → Add______Follower
"이 사용자에서 팔로워를 제거" → Remove______Follower
힌트: 관리 리소스는 팔로우를 받는 사용자입니다. (정답: AddUserFollower / RemoveUserFollower)
독립 적용 (혼자서)
문제: "Alice가 engineering 그룹에서 admin 역할을 갖는다"를 15장 패턴으로 표현할 수 있나요?
생각해 볼 점: 15장은 관계의 존재 여부만 저장합니다. admin은 역할, 즉 관계 메타데이터죠. (답: 표현할 수 없습니다. 역할은 메타데이터라서 14장 연관 리소스의 role 필드가 필요합니다.)
한 줄 정리
- 관계에 적을 정보가 없으면, 멤버십 카드(14장) 대신 더하기·빼기(15장)로 단순하게.
- add/remove는 한쪽(관리 리소스)에만 붙인다 — "A에 B를 넣는다"가 자연스러운 쪽이 A.
- 본문엔 식별자만, 목록은 양방향(User[]/Group[]), 주소는 암시적 서브컬렉션.
- 중복 추가 409, 없는 것 제거 412, 없는 대상 404. 멱등성 덕에 409도 사실 목적 달성.
- 대가는 비상호관계(한쪽만 가능)와 메타데이터 못 적음. 정보가 필요하면 14장으로.
다음 장 예고: 16장에서는 한 자리에 여러 타입이 올 수 있는 다형성을 다룹니다. 지금 몰라도 됩니다.
클릭하거나 Space를 눌러 뒤집기